﻿using System;
using System.Collections.Generic;
using System.IO;
using FM_Mono;
using UnityEngine;
using UnityEngine.Profiling;
using FibMatrix.DS;
using FibMatrix.IO;
using FibMatrix.Schedule;
using FibMatrix.Utils;

namespace FibMatrix
{
    public class ResourceDownloadManager
    {
        /// <summary>
        /// 文件下载信息
        /// </summary>
        private class ResourceDownloadUnit : IDisposable
        {
            public string srcUrl; // 构建时文件路径
            public string lockStoragePath;
            public string storagePath;
            public uint crc32;
            public string version; // md5
            public ulong size;
            public ulong compressedSize;

            public void Dispose()
            {
            }
        }

        private const long READ_PRELOAD_FILE_FRAME_TIME = 10;

        private const int MAX_ERROR_TIME = 5;

        private const int MAX_TOTAL_ERROR_TIME = 20;

        private static ResourceDownloadManager _instance;

        [AcquireOnce]
        public static ResourceDownloadManager instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = new ResourceDownloadManager();
                }

                return _instance;
            }
        }

        /// <summary>
        /// 下载任务，负责回调
        /// </summary>
        private class ResourceDownloadTask : IResourceDownloadTask, IDisposable
        {
            private Action<IAsyncBase> _asyncCallback;

            public event Action<IAsyncBase> completeCallback
            {
                add { this._asyncCallback += value; }
                remove { this._asyncCallback -= value; }
            }

            private bool _completed = false;

            public bool isCompleted
            {
                get { return _completed; }
            }

            public float progress
            {
                get { return this._completed ? 1 : 0; }
            }

            public string error { get; set; }

            public string storagePath { get; set; }

            public void DispatchAsyncOperationComplete()
            {
                this._completed = true;
                Action<IAsyncBase> callback = this._asyncCallback;
                this._asyncCallback = null;
                if (callback != null)
                {
                    callback(this);
                }
            }

            public void Dispose()
            {
                this._asyncCallback = null;
                this._completed = false;
                this.error = null;
            }
        }

        private ObjectPool2<ResourceDownloadUnit> _unitPool =
            new ObjectPool2<ResourceDownloadUnit>(-1, () => new ResourceDownloadUnit());

        private ObjectPool2<ResourceDownloadTask> _downloadTaskPool =
            new ObjectPool2<ResourceDownloadTask>(-1, () => new ResourceDownloadTask());

        // 高优先级待下载队列，从这里挑选文件转入下载(loadingTask)，比如正要使用的文件请求会进入这里。注意不是资源路径
        private Queue<string> _l1Queue = new Queue<string>();

        // 低优先级待下载队列，从这里转入L1，比如启动后的下载全部会进入这里。注意不是资源路径
        private List<string> _l2Queue = new List<string>();

        // 构建文件路径到下载信息的映射，为了缓存减少创建。注意不是资源路径
        private Dictionary<string, ResourceDownloadUnit> _queueUnitsMap =
            new Dictionary<string, ResourceDownloadUnit>();

        // 构建文件路径到请求任务的映射，对同一个文件的多个请求会返回同一个任务
        private Dictionary<string, ResourceDownloadTask> _pendingCallbacks =
            new Dictionary<string, ResourceDownloadTask>();

        // 当前未完成的文件下载请求，可保证同一文件不多次下载. 数量 = _l1Queue + loadingTask
        private HashSet<string> _pendingTasks = new HashSet<string>();

        // 正在执行下载的文件
        private Dictionary<string, ResourceDownloader> _loadingTasks = new Dictionary<string, ResourceDownloader>();

        private Dictionary<string, int> _errorTimeMap = new Dictionary<string, int>();

        private Dictionary<string, int> _totalErrorTimeMap = new Dictionary<string, int>();

        // 如果文件下载失败，重试时会换到别的cdn
        private Dictionary<string, int> _downdURLIndexMap = new Dictionary<string, int>();

        private Dictionary<string, ResourceDownloadUnit> _failedUnits = new Dictionary<string, ResourceDownloadUnit>();

        private const int k_MaxL1QueueConcurrentTask = 16;

        private const int k_MaxL2QueueConcurrentTask = 1;

        private int _curL1QueueConcurrentTask = 0;

        private int _curL2QueueConcurrentTask = 0;

        private bool _inited = false;

        private Action<string> _errorFileCallBack = null;

        // 可在游戏开始阶段全部下载的资源，注意不是文件路径，是资源路径
        private List<string> _preloadAssets = new List<string>();

        private List<string> _cdnList = new List<string>();

        private AssetList _assetConfig = null; // 当前正在使用的资源列表，可能是包内，也可能是包外
        private AssetList _assetCacheConfig = null; // 强制重下热更时保留下来之前的资源列表，可减少新下载

        // 记录该轮到哪个preload进入L2队列
        private int _preloadIndex;

        private int _frameCollectHandler = -1;
        private int _downloadUpdator = -1;

        private string _temporaryCachePath; // 下载文件的临时目录

        private string _persistentPath;
        private string _persistentCachePath;

        private ResourceDownloadManager()
        {
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="preloadAssets">工程里资源的路径，可能多个对应一个bundle文件路径</param>
        /// <param name="assetConfig"></param>
        /// <param name="cdnList"></param>
        /// <param name="persistentPath"></param>
        /// <param name="temporaryCachePath"></param>
        public void Init(HashSet<string> preloadAssets, AssetConfig assetConfig, List<string> cdnList,
            string persistentPath, string temporaryCachePath)
        {
            if (_inited)
            {
                Reset();
            }

            _assetConfig = assetConfig.resList;
            _cdnList.Clear();
            if (ResourceDevCfg.Instance.IsDebugStreamPlay())
            {
                _cdnList.Add(ResourceDevCfg.Instance.GetDebugCdn());
#if !UNITY_WEBGL
                // webgl不用调试时指定下载所有资源，除了需要同步预加载的都要下载
                preloadAssets.Clear();
                foreach (var v in _assetConfig.nodes)
                {
                    if (v.id.EndsWith(".dll") == false)
                        preloadAssets.Add(v.id);
                }
#endif
            }
            else
                _cdnList.AddRange(cdnList);

            _preloadAssets.Clear();
            _preloadAssets.AddRange(preloadAssets);
            Debug.Log($"preload asset(not file) count: {_preloadAssets.Count}");


            _inited = true;
            _pendingCallbacks.Clear();
            _errorTimeMap.Clear();
            _failedUnits.Clear();
            _l1Queue.Clear();
            _l2Queue.Clear();
            _pendingTasks.Clear();
            _loadingTasks.Clear();
            _totalErrorTimeMap.Clear();
            _downloadUpdator = GlobalScheduler.AddFrameLimitUpdator(1, OnUpdateDownload);

            this._persistentPath = Path.Combine(persistentPath, ResourcePathConst.REMOTE_ASSET_DIRECTORY);
            this._persistentCachePath = Path.Combine(persistentPath, ResourcePathConst.CACHE_ASSET_DIRECTORY);
            this._temporaryCachePath = Path.Combine(temporaryCachePath, ResourcePathConst.TEMP_ASSET_DIRECTORY);
            if (StorageManager.Exists(_persistentCachePath))
            {
                AssetConfig cac = AssetConfig.LoadFromDisk(
                    Path.Combine(_persistentCachePath, AssetConfig.ASSET_CONFIG_FILE_NAME));
                if (cac != null)
                {
                    _assetCacheConfig = cac.resList;
                }
            }
            else
            {
                _persistentCachePath = null;
            }
        }

        public void Reset()
        {
            if (!_inited)
            {
                return;
            }

            _inited = false;

            GlobalScheduler.RemoveLimitUpdator(ref _frameCollectHandler);
            GlobalScheduler.RemoveLimitUpdator(ref _downloadUpdator);

            if (_loadingTasks.Count > 0)
            {
                var iter = this._loadingTasks.GetEnumerator();
                while (iter.MoveNext())
                {
                    iter.Current.Value.Dispose();
                }

                this._loadingTasks.Clear();
            }

            this._pendingTasks.Clear();
            this._pendingCallbacks.Clear();
            this._l1Queue.Clear();
            this._l2Queue.Clear();
            this._totalErrorTimeMap.Clear();
            this._errorTimeMap.Clear();

            if (_queueUnitsMap.Count > 0)
            {
                var iter = this._queueUnitsMap.GetEnumerator();
                while (iter.MoveNext())
                {
                    this._unitPool.Recycle(iter.Current.Value);
                }

                this._queueUnitsMap.Clear();
            }

            this._preloadIndex = 0;
        }

        public void DownloadAll()
        {
#if UNITY_WEBGL
            // 小游戏暂时不执行静默下载，跑通调优时再看
            return;
#endif
            if (!this._inited)
            {
                Debug.LogError("not inited, can't download all");
                return;
            }

            if (!GlobalScheduler.IsValid(this._frameCollectHandler))
            {
                _preloadIndex = 0;
                _frameCollectHandler = GlobalScheduler.AddFrameLimitUpdator(1, this.CollectPreloadFilePerframe);
                Debug.Log($"DownloadAll");
            }
        }

        private void DelayCallTaskComplete(object context)
        {
            var task = context as ResourceDownloadTask;
            task.DispatchAsyncOperationComplete();
            this._downloadTaskPool.Recycle(task);
        }

        public IResourceDownloadTask DownloadSingle(AssetNode item)
        {
            if (!_inited || item == null)
            {
                Debug.LogError($"can't download, inited= {_inited}, url= {item?.url}");
                return null;
            }

            Profiler.BeginSample("TryDownloadSingle");
            Profiler.BeginSample(item.url);
            string bundleUrl = Path.Combine(_persistentPath, ResourceHelper.GetRuntimeSavePath(item.url, item.md5));
            string lockBundleUrl = bundleUrl + ".lock";
            if (!_queueUnitsMap.ContainsKey(item.url))
                AddUnitToQueueMap(item, bundleUrl, lockBundleUrl);

            ResourceDownloadTask downloadTask = null;
            // 已经存在就不下载，创个任务下一帧回调完成
            if (StorageManager.Exists(bundleUrl) && !StorageManager.Exists(lockBundleUrl))
            {
                downloadTask = this._downloadTaskPool.Allocate();
                downloadTask.storagePath = bundleUrl;
                downloadTask.error = null;
                GlobalScheduler.CallOnceFrame(1, this.DelayCallTaskComplete, downloadTask);
            }
            else if (TryCopyCacheToBundle(item, bundleUrl, lockBundleUrl))
            {
                downloadTask = this._downloadTaskPool.Allocate();
                downloadTask.storagePath = bundleUrl;
                downloadTask.error = null;
                GlobalScheduler.CallOnceFrame(1, this.DelayCallTaskComplete, downloadTask);
            }
            else
            {
                Debug.Log($"DownloadSingle L1:{_l1Queue.Count}, L2:{_l2Queue.Count}, {item.url}");
                // 用文件表示唯一下载，而不是资源路径
                string key = item.url;
                //重置下载失败状态
                _errorTimeMap[key] = 0;
                this._failedUnits.Remove(key);

                //已经存在正在下载的任务
                if (_pendingTasks.Contains(key))
                {
                    //已经存在的下载任务是queue任务, 尽量用同一个callback, 对于当前执行的队列任务数减一，同时触发一下queueDownload
                    if (!_pendingCallbacks.ContainsKey(key))
                    {
                        downloadTask = _pendingCallbacks[key] = this._downloadTaskPool.Allocate();
                    }
                    else
                    {
                        downloadTask = _pendingCallbacks[key];
                    }
                }
                else
                {
                    //缓存回调函数
                    downloadTask = _pendingCallbacks[key] = this._downloadTaskPool.Allocate();
                    this._pendingTasks.Add(key);
                    this._l2Queue.Remove(key); // 从L2移除，不重复下载。小游戏时同一帧大量资源请求性能不好
                    this._l1Queue.Enqueue(key);
                }
            }

            Profiler.EndSample();
            Profiler.EndSample();
            return downloadTask;
        }

        /// <summary>
        /// 把preload资源列表对应的文件分帧加到L2队列中
        /// </summary>
        /// <param name="data"></param>
        private void CollectPreloadFilePerframe(object data)
        {
            Profiler.BeginSample("CollectPreloadFilePerframe");
            int size = this._preloadAssets.Count;
            long now = FibMatrix.RealTimer.elapsedMilliseconds;
            for (; this._preloadIndex < size;)
            {
                string assetPath = this._preloadAssets[this._preloadIndex];
                ++this._preloadIndex;
                // Debug.Log($"CollectPreloadFilePerframe begin to download {assetPath}");
                if (!_assetConfig.Contains(assetPath))
                {
                    Debug.LogError($"CollectPreloadFilePerframe {assetPath} doest exist in asset config");
                    continue;
                }

                // TODO, 底包里有的文件就不用下了吧
                // 资源依赖的文件
                var depIds = ResourceHelper.GetAllDependenciesUrlByAssets(_assetConfig,
                    new string[] { assetPath }, false);
                foreach (var v in depIds)
                {
                    if (!_assetConfig.TryGetValue(v, out AssetNode item))
                    {
                        Debug.LogError($"CollectPreloadFilePerframe dep file not found: {v}");
                        continue;
                    }
                    // 多个资源对应的同一文件只下载一次
                    if (!this._queueUnitsMap.ContainsKey(item.url))
                    {
                        
                        string bundleUrl = Path.Combine(_persistentPath, ResourceHelper.GetRuntimeSavePath(item.url, item.md5));
                        string lockBundleUrl = bundleUrl + ".lock";
                        if (!StorageManager.Exists(bundleUrl) || StorageManager.Exists(lockBundleUrl))
                        {
                            if (!TryCopyCacheToBundle(item, bundleUrl, lockBundleUrl))
                            {
                                AddUnitToQueueMap(item, bundleUrl, lockBundleUrl);
                                this._l2Queue.Add(item.url);
                                Debug.Log($"CollectPreloadFilePerframe add to l2queue {item.url}");
                            }
                        }
                    }
                }

                long elapsed = FibMatrix.RealTimer.elapsedMilliseconds - now;
                if (elapsed > READ_PRELOAD_FILE_FRAME_TIME)
                {
                    break;
                }
            }

            if (this._preloadIndex >= size)
            {
                if (_persistentCachePath != null)
                    StorageManager.DeleteDirectory(_persistentCachePath);
                GlobalScheduler.RemoveLimitUpdator(ref this._frameCollectHandler);
            }

            Profiler.EndSample();
        }

        void AddUnitToQueueMap(AssetNode item, string bundleUrl, string lockBundleUrl)
        {
            ResourceDownloadUnit unit = _unitPool.Allocate();
            unit.srcUrl = item.url;
            unit.size = item.size;
            unit.crc32 = item.crc32;
            unit.version = item.md5;
            unit.storagePath = bundleUrl;
            unit.lockStoragePath = lockBundleUrl;
            this._queueUnitsMap.Add(item.url, unit);
        }

        /// <summary>
        /// 尝试把之前的cache文件辅助为bundle，可免去下载
        /// </summary>
        /// <param name="item"></param>
        /// <param name="bundleUrl"></param>
        /// <param name="lockBundleUrl">用lock文件表示copy或move时出问题</param>
        /// <returns>是否成功</returns>
        bool TryCopyCacheToBundle(AssetNode item, string bundleUrl, string lockBundleUrl)
        {
            bool useCache = false;
            if (_assetCacheConfig != null)
            {
                _assetCacheConfig.TryGetValue(item.id, out AssetNode cacheNode);
                useCache = cacheNode != null && cacheNode.md5 == item.md5;
            }

            if (useCache)
            {
                string cachedFilePath = Path.Combine(_persistentCachePath, item.url);
                if (StorageManager.Exists(cachedFilePath))
                {
                    if (!StorageManager.Exists(lockBundleUrl))
                    {
                        using (StorageManager.CreateFile(lockBundleUrl)) ;
                    }

                    StorageManager.CopyFile(cachedFilePath, bundleUrl, true);
                    StorageManager.DeleteFile(lockBundleUrl);
                    StorageManager.DeleteFile(cachedFilePath);
                }
                else
                {
                    useCache = false;
                }
            }

            return useCache;
        }

        /// <summary>
        /// 有很多请求在排队时，靠它选出请求进行下载
        /// </summary>
        /// <param name="data"></param>
        private void OnUpdateDownload(object data)
        {
            // l1的排队任务很少时才能调度l2任务
            if (_l1Queue.Count <= k_MaxL1QueueConcurrentTask)
                TryMoveL2ToL1();

            TryL1QueueDowload();
        }

        private void TryMoveL2ToL1()
        {
            if (this._l2Queue.Count == 0 && _preloadIndex == _preloadAssets.Count && _failedUnits.Count > 0)
            {
                DownloadFailedAssets();
                return;
            }

            //! Current concurrent task count
            while (_curL2QueueConcurrentTask < k_MaxL2QueueConcurrentTask && _l2Queue.Count > 0)
            {
                string key = _l2Queue[0];
                _l2Queue.RemoveAt(0); // 性能不好
                _curL2QueueConcurrentTask++;
                this._pendingTasks.Add(key);
                this._l1Queue.Enqueue(key);
                // Debug.Log($"L2QueueDowload add to l1queue {key}");
            }
        }


        private void TryL1QueueDowload()
        {
            Profiler.BeginSample("TryL1QueueDowload");
            //! Current concurrent task count
            while (_curL1QueueConcurrentTask < k_MaxL1QueueConcurrentTask && _l1Queue.Count > 0)
            {
                string key = _l1Queue.Dequeue();

                ResourceDownloadUnit unit;
                if (!this._queueUnitsMap.TryGetValue(key, out unit))
                {
                    Debug.LogError($"why: {key}");
                    continue;
                }

                _curL1QueueConcurrentTask++;

                string srcUrl = unit.srcUrl;
                int urlIndex;
                if (!this._downdURLIndexMap.TryGetValue(key, out urlIndex))
                {
                    srcUrl = ResourceHelper.GetAssetURL(this._cdnList[0], srcUrl, unit.version);
                    this._downdURLIndexMap.Add(key, 1);
                }
                else
                {
                    srcUrl = ResourceHelper.GetAssetURL(this._cdnList[urlIndex % this._cdnList.Count],
                        srcUrl, unit.version);
                    this._downdURLIndexMap[key] = urlIndex + 1;
                }

                ResourceDownloader downloader = ResourceDownloader.StartNew(srcUrl, unit.size, unit.compressedSize,
                    unit.crc32, Path.Combine(_temporaryCachePath, Guid.NewGuid().ToString()),
                    this.OnDownloaderComplete, key);
                this._loadingTasks.Add(key, downloader);
                Debug.Log($"L1QueueDowload start, ct= {_loadingTasks.Count}, {srcUrl}");
            }

            Profiler.EndSample();
        }

        private void OnDownloaderComplete(IAsyncBase task)
        {
            Profiler.BeginSample("OnDownloaderComplete");
            var downloader = task as ResourceDownloader;
            var key = downloader.context as string;
            bool succ = false;

            _curL1QueueConcurrentTask = Math.Max(0, _curL1QueueConcurrentTask - 1);
            _curL2QueueConcurrentTask = Math.Max(0, _curL2QueueConcurrentTask - 1);


            if (!string.IsNullOrEmpty(key))
            {
                if (string.IsNullOrEmpty(downloader.error))
                {
                    succ = OnFileSuccess(key, downloader);
                }
                else
                {
                    succ = false;
                    OnFileError(key, downloader.error, downloader);
                }
            }
            else
                Debug.LogError($"unknown download finish: {downloader.url}");


            this._loadingTasks.Remove(key);
            this._pendingTasks.Remove(key);
            // unitMap应该也可以移除，不用一直存着
            ResourceDownloadUnit unit = _queueUnitsMap[key];

            //如果这个文件有回调函数, 则是downloadSingle触发的，执行回调
            if (this._pendingCallbacks.TryGetValue(key, out ResourceDownloadTask rdTask))
            {
                this._pendingCallbacks.Remove(key);
                rdTask.storagePath = succ ? unit.storagePath : null;
                rdTask.error = downloader.error;
                rdTask.DispatchAsyncOperationComplete();
                this._downloadTaskPool.Recycle(rdTask);
            }

            downloader.Dispose();
            Profiler.EndSample();
        }

        private bool OnFileSuccess(string key, ResourceDownloader downloader)
        {
            Debug.Log($"download succ: {key}");
            ResourceDownloadUnit unit = _queueUnitsMap[key];
            string lockFileName = unit.lockStoragePath;
            string moveError = null;
            try
            {
                StorageManager.CreateDirectoryIfNotExist(unit.storagePath);
                StorageManager.CreateDirectoryIfNotExist(unit.lockStoragePath);
                if (!StorageManager.Exists(lockFileName))
                {
                    using (StorageManager.CreateFile(lockFileName)) ;
                }

                // 临时下载目录 to 存储目录
                StorageManager.MoveFile(downloader.storagePath, unit.storagePath);
                StorageManager.DeleteFile(lockFileName);
            }
            catch (Exception e)
            {
                Debug.LogException(e);
                moveError = e.ToString();
            }

            if (moveError != null)
            {
                try
                {
                    StorageManager.DeleteFile(unit.storagePath);
                }
                catch (Exception e)
                {
                    Debug.LogException(e);
                }

                this.OnFileError(key, moveError, downloader);
                return false;
            }

            if (this._failedUnits.ContainsKey(key))
            {
                this._failedUnits.Remove(key);
            }

            _errorTimeMap[key] = 0;
            _totalErrorTimeMap[key] = 0;
            return true;
        }


        private void OnFileError(string key, string error, ResourceDownloader downloader)
        {
            Debug.LogError($"download failed: {key}, {error}");

            int totalTime;
            _totalErrorTimeMap.TryGetValue(key, out totalTime);
            int lastTotalTime = totalTime;
            int curTime;
            _errorTimeMap.TryGetValue(key, out curTime);
            if (++totalTime <= MAX_TOTAL_ERROR_TIME)
            {
                if (++curTime <= MAX_ERROR_TIME)
                {
                    // 重下
                    _l2Queue.Add(key);
                }
                else
                {
                    //不重新下了 丢到错误列表 本次队列全完成后再来下这个文件
                    if (!_failedUnits.ContainsKey(key))
                    {
                        ResourceDownloadUnit unit = _queueUnitsMap[key];
                        _failedUnits.Add(key, unit);
                    }
                }
            }
            else if (lastTotalTime <= MAX_TOTAL_ERROR_TIME)
            {
                //不重新下了 重试MAX_TOTAL_ERROR_TIME次后还是出错
                _errorFileCallBack?.Invoke(error);
                //        SE_LOGE("download error %s\n", identifier.c_str());
            }

            _totalErrorTimeMap[key] = totalTime;
            _errorTimeMap[key] = curTime;
        }

        private void DownloadFailedAssets()
        {
            _l2Queue.Clear();
            _curL2QueueConcurrentTask = 0;
            _errorTimeMap.Clear();
            var iter = this._failedUnits.GetEnumerator();
            while (iter.MoveNext())
            {
                _l2Queue.Add(iter.Current.Key);
            }

            _failedUnits.Clear();
        }
    }
}